#--Version 1.0.0
#--Nathan Denny, May 27, 1999

class Graph_topological_error(Exception):
        pass

class GraphQueue:
	def __init__(self):
		self.q=[]

	def empty(self):
		if(len(self.q)>0):
			return 0
		else:
			return 1

	def count(self):
		return len(self.q)

	def add(self, item):
		self.q.append(item)	

	def remove(self):
		item=self.q[0]
		self.q=self.q[1:]
		return item

class GraphStack:
	def __init__(self):
		self.s=[]

	def empty(self):
		if(len(self.s)>0):
			return 0
		else:
			return 1
	
	def count(self):
		return len(self.s)

	def push(self, item):
		ts=[item]
		for i in self.s:
			ts.append(i)
		self.s=ts

	def pop(self):
		item=self.s[0]
		self.s=self.s[1:]
		return item

class Graph:
	def __init__(self):
		self.next_edge_id=0
		self.nodes={}
		self.edges={}
		self.hidden_edges={}
		self.hidden_nodes={}

	#--Performs a copy of the graph, G, into self.
	#--hidden edges and hidden nodes are not copied.
	#--node_id's remain consistent across self and G, 
	#--however edge_id's do not remain consistent.
	#--Need to implement copy operator on node_data
	#--and edge data.
	def copy(self, G):
	    #--Blank self.
	    self.nodes={}
	    self.edges={}
	    self.hidden_edges={}
	    self.hidden_nodes={}
	    self.next_edge_id=0
	    #--Copy nodes.
	    G_node_list=G.node_list()
	    for G_node in G_node_list:
		self.add_node(G_node,G.node_data(G_node))
	    #--Copy edges.
	    for G_node in G_node_list:
		out_edges=G.out_arcs(G_node)
		for edge in out_edges:
		    tail_id=G.tail(edge)
		    self.add_edge(G_node, tail_id, G.edge_data(edge))

	#--Creates a new node with id node_id.  Arbitrary data can be attached
	#--to the node viea the node_data parameter.
	def add_node(self, node_id, node_data=None):
		if (not self.nodes.has_key(node_id)) and (not self.hidden_nodes.has_key(node_id)):
			self.nodes[node_id]=([],[],node_data)
		else:
			#print "WARNING: Duplicate node id's.  Latest node id was ignored."
			raise Graph_duplicate_node, node_id

	#--Deletes the node and all in and out arcs.
	def delete_node(self, node_id):
		#--Remove fanin connections.
		in_edges=self.in_arcs(node_id)
		for edge in in_edges:
			self.delete_edge(edge)
		#--Remove fanout connections.
		out_edges=self.out_arcs(node_id)
		for edge in out_edges:
			self.delete_edge(edge)
		#--Delete node.
		del self.nodes[node_id]
		
	#--Delets the edge.
	def delete_edge(self, edge_id):
		head_id=self.head(edge_id)
		tail_id=self.tail(edge_id)
		head_data=map(None, self.nodes[head_id])
		tail_data=map(None, self.nodes[tail_id])
		head_data[1].remove(edge_id)
		tail_data[0].remove(edge_id)
		del self.edges[edge_id]
	
	#--Adds an edge (head_id, tail_id).
	#--Arbitrary data can be attached to the edge via edge_data
	def add_edge(self, head_id, tail_id, edge_data=None):
		edge_id=self.next_edge_id
		self.next_edge_id=self.next_edge_id+1
		self.edges[edge_id]=(head_id, tail_id, edge_data)
		mapped_head_data=map(None, self.nodes[head_id])
		mapped_head_data[1].append(edge_id)
		mapped_tail_data=map(None, self.nodes[tail_id])
		mapped_tail_data[0].append(edge_id)
		return edge_id

	#--Removes the edge from the normal graph, but does not delete
	#--its information.  The edge is held in a separate structure
	#--and can be unhidden at some later time.
	def hide_edge(self, edge_id):
		self.hidden_edges[edge_id]=self.edges[edge_id]
		ed=map(None, self.edges[edge_id])
		head_id=ed[0]
		tail_id=ed[1]
		hd=map(None, self.nodes[head_id])
		td=map(None, self.nodes[tail_id])
		hd[1].remove(edge_id)
		td[0].remove(edge_id)
		del self.edges[edge_id]
		
	#--Similar to above.
	#--Stores a tuple of the node data, and the edges that are incident to and from
	#--the node.  It also hides the incident edges.
	def hide_node(self, node_id):	    
		degree_list=self.arc_list(node_id)
		self.hidden_nodes[node_id]=(self.nodes[node_id],degree_list)
		for edge in degree_list:
			self.hide_edge(edge)
		del self.nodes[node_id]

	#--Restores a previously hidden edge back into the graph.
	def restore_edge(self, edge_id):
		self.edges[edge_id]=self.hidden_edges[edge_id]
		ed=map(None,self.hidden_edges[edge_id])
		head_id=ed[0]
		tail_id=ed[1]
		hd=map(None,self.nodes[head_id])
		td=map(None,self.nodes[tail_id])
		hd[1].append(edge_id)
		td[0].append(edge_id)
		del self.hidden_edges[edge_id]

	#--Restores all hidden edges.
	def restore_all_edges(self):
		hidden_edge_list=self.hidden_edges.keys()
		for edge in hidden_edge_list:
			self.restore_edge(edge)

	#--Restores a previously hidden node back into the graph
	#--and restores all of the hidden incident edges, too.	
	def restore_node(self, node_id):
		hidden_node_data=map(None,self.hidden_nodes[node_id])
		self.nodes[node_id]=hidden_node_data[0]
		degree_list=hidden_node_data[1]
		for edge in degree_list:
			self.restore_edge(edge)
		del self.hidden_nodes[node_id]
	
	#--Restores all hidden nodes.
	def restore_all_nodes(self):
		hidden_node_list=self.nodes.keys()
		for node in hidden_node_list:
			self.nodes[node]=self.hidden_nodes[node]
			del self.hidden_nodes[node]

	#--Returns 1 if the node_id is in the graph and 0 otherwise.
	def has_node(self, node_id):
		if self.nodes.has_key(node_id):
			return 1
		else:
			return 0

	#--Returns the edge that connects (head_id,tail_id)
	def edge(self, head_id, tail_id):
		out_edges=self.out_arcs(head_id)
	    	for edge in out_edges:
			if self.tail(edge)==tail_id:
		    		return edge
		raise Graph_no_edge, (head_id, tail_id)
		#print "WARNING: No edge to return."

	def number_of_nodes(self):
	    return len(self.nodes.keys())

	def number_of_edges(self):
	    return len(self.edges.keys())

	#--Return a list of the node id's of all visible nodes in the graph.
	def node_list(self):
		nl=self.nodes.keys()
		return nl[:]	

	#--Similar to above.
	def edge_list(self):
		el=self.edges.keys()
		return el[:]

	def number_of_hidden_edges(self):
		return len(self.hidden_edges.keys())

	def number_of_hidden_nodes(self):
		return len(self.hidden_nodes.keys())

	def hidden_node_list(self):
		hnl=self.hidden_nodes.keys()
		return hnl[:]

	def hidden_edge_list(self):
		hel=self.hidden_edges.keys()
		return hel[:]

	#--Returns a reference to the data attached to a node.
	def node_data(self, node_id):
		mapped_data=map(None, self.nodes[node_id])
		return mapped_data[2]

	#--Returns a reference to the data attached to an edge.
	def edge_data(self, edge_id):
		mapped_data=map(None, self.edges[edge_id])
		return mapped_data[2]

	#--Returns a reference to the head of the edge.  (A reference to the head id)
	def head(self, edge):
		mapped_data=map(None, self.edges[edge])
		return mapped_data[0]	

	#--Similar to above.
	def tail(self, edge):
		mapped_data=map(None, self.edges[edge])
		return mapped_data[1]

	#--Returns a copy of the list of edges of the node's out arcs.
	def out_arcs(self, node_id):
		mapped_data=map(None, self.nodes[node_id])
		return mapped_data[1][:]	

	#--Similar to above.
	def in_arcs(self, node_id):
		mapped_data=map(None, self.nodes[node_id])
		return mapped_data[0][:]

	#--Returns a list of in and out arcs.
	def arc_list(self, node_id):
		in_list=self.in_arcs(node_id)
		out_list=self.out_arcs(node_id)
		deg_list=[]
		for arc in in_list:
			deg_list.append(arc)
		for arc in out_list:
			deg_list.append(arc)
		return deg_list
	    

	def out_degree(self, node_id):
		mapped_data=map(None, self.nodes[node_id])
		return len(mapped_data[1])

	def in_degree(self, node_id):
		mapped_data=map(None, self.nodes[node_id])
		return len(mapped_data[0])

	def degree(self, node_id):
		mapped_data=map(None, self.nodes[node_id])
		return len(mapped_data[0])+len(mapped_data[1])	

	# --- Traversals ---

	#--Performs a topological sort of the nodes by "removing" nodes with indegree 0.
	#--If the graph has a cycle, the Graph_topological_error is thrown with the
	#--list of successfully ordered nodes.
	def topological_sort(self):
		topological_list=[]
		topological_queue=GraphQueue()
		indeg_nodes={}
		node_list=self.nodes.keys()
		for node in node_list:
			indeg=self.in_degree(node)
			if indeg==0:
				topological_queue.add(node)
			else:
				indeg_nodes[node]=indeg
		while not topological_queue.empty():
			current_node=topological_queue.remove()
			topological_list.append(current_node)
			out_edges=self.out_arcs(current_node)
			for edge in out_edges:
				tail=self.tail(edge)
				indeg_nodes[tail]=indeg_nodes[tail]-1
				if indeg_nodes[tail]==0:
					topological_queue.add(tail)
		#--Check to see if all nodes were covered.
		if len(topological_list)!=len(node_list):
			#print "WARNING: Graph appears to be cyclic. Topological sort is invalid!"
			raise Graph_topological_error, topological_list
		return topological_list

	#--Performs a reverse topological sort by iteratively "removing" nodes with out_degree=0
	#--If the graph is cyclic, this method throws Graph_topological_error with the list of
	#--successfully ordered nodes.
	def reverse_topological_sort(self):
		topological_list=[]
		topological_queue=GraphQueue()
		outdeg_nodes={}
		node_list=self.nodes.keys()
		for node in node_list:
			outdeg=self.out_degree(node)
			if outdeg==0:
				topological_queue.add(node)
			else:
				outdeg_nodes[node]=outdeg
		while not topological_queue.empty():
			current_node=topological_queue.remove()
			topological_list.append(current_node)			
			in_edges=self.in_arcs(current_node)
			for edge in in_edges:
				head_id=self.head(edge)
				outdeg_nodes[head_id]=outdeg_nodes[head_id]-1
				if outdeg_nodes[head_id]==0:
					topological_queue.add(head_id)
		#--Sanity check.
		if len(topological_list)!=len(node_list):
			raise Graph_topological_error, topological_list
		return topological_list

	#--Returns a list of nodes in some DFS order.
	def dfs(self, source_id):
		nodes_already_stacked={source_id:0}
		dfs_list=[]		

		dfs_stack=GraphStack()
		dfs_stack.push(source_id)

		while not dfs_stack.empty():
			current_node=dfs_stack.pop()
			dfs_list.append(current_node)
			out_edges=self.out_arcs(current_node)
			for edge in out_edges:
				if not nodes_already_stacked.has_key(self.tail(edge)):
					nodes_already_stacked[self.tail(edge)]=0
					dfs_stack.push(self.tail(edge))
		return dfs_list

	#--Returns a list of nodes in some BFS order.
	def bfs(self, source_id):
		nodes_already_queued={source_id:0}
		bfs_list=[]
	
		bfs_queue=GraphQueue()
		bfs_queue.add(source_id)	
	
		while not bfs_queue.empty():
			current_node=bfs_queue.remove()
			bfs_list.append(current_node)
			out_edges=self.out_arcs(current_node)
			for edge in out_edges:
				if not nodes_already_queued.has_key(self.tail(edge)):
					nodes_already_queued[self.tail(edge)]=0
					bfs_queue.add(self.tail(edge))
		return bfs_list


	#--Returns a list of nodes in some BACKWARDS BFS order.
	#--Starting from the source node, BFS proceeds along back edges.
	def back_bfs(self, source_id):
		nodes_already_queued={source_id:0}
		bfs_list=[]
	
		bfs_queue=GraphQueue()
		bfs_queue.add(source_id)

		while not bfs_queue.empty():
			current_node=bfs_queue.remove()
			bfs_list.append(current_node)
			in_edges=self.in_arcs(current_node)
			for edge in in_edges:
				if not nodes_already_queued.has_key(self.head(edge)):
					nodes_already_queued[self.head(edge)]=0
					bfs_queue.add(self.head(edge))
		return bfs_list
